package com.nado.extensions.util;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.xml.utils.DefaultErrorHandler;
import org.apache.xpath.CachedXPathAPI;
import org.apache.xpath.XPathAPI;
import org.w3c.dom.*;
import org.w3c.dom.Element;
import org.xml.sax.EntityResolver;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.TransformerException;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.StringWriter;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.WeakHashMap;

/**
 * {@link File} 또는 {@link InputStream}으로부터 {@link Document}를 생성하고,
 * 생성된 {@link Document}를 이용하여 XML 정보를 검색하는 데 사용되는 클래스.<p>
 * {@link Document} 생성 부분을 제외한 검색 API는 모두 deprecated되었으므로, 검색 위주
 * 의 API를 사용하는 경우에는 {@link JaxpXmlDataHandler} 또는{@link Dom4jXmlDataHandler}
 * 를 사용할 것을 권장한다.
 */
public class XMLDOMUtil {

	protected static final Log log = LogFactory.getLog(XMLDOMUtil.class);

	/**
	 * XPath 구분의 빠른 검색을 위한 {@link CachedXPathAPI} {@link Map}
	 */
	private static final Map<Document, CachedXPathAPI> cachedXPathAPIs =
			Collections.synchronizedMap(new WeakHashMap<Document, CachedXPathAPI>());

	/**
	 * JAXP attribute used to configure the schema language for validation.
	 */
	private static final String SCHEMA_LANGUAGE_ATTRIBUTE =
			"http://java.sun.com/xml/jaxp/properties/schemaLanguage";

	/**
	 * JAXP attribute value indicating the XSD schema language.
	 */
	private static final String XSD_SCHEMA_LANGUAGE =
			"http://www.w3.org/2001/XMLSchema";

	/**
	 * {@link File}을 이용하여 {@link Document}를 생성한다.
	 * XML에 대한 유효성 검증을 수행하지 않는다.
	 *
	 * @param file XML 정보를 담고 있는 {@link File}
	 * @throws Exception {@link Document} 생성 Exception
	 */
	public static Document loading(File file) throws Exception {
		return loading(new FileInputStream(file));
	}

	/**
	 * {@link InputStream}을 이용하여 {@link Document}를 생성한다.
	 * XML에 대한 유효성 검증을 수행하지 않는다.
	 *
	 * @param stream XML 정보를 담고 있는 {@link InputStream}
	 * @throws Exception {@link Document} 생성 Exception
	 */
	public static Document loading(InputStream stream) throws Exception {
		return loading(stream, true);
	}

	/**
	 * 파일 정보를 이용하여 {@link Document}를 생성한다.
	 * XML에 대한 유효성 검증을 수행하지 않는다.
	 *
	 * @param location	   XML 파일 경로
	 * @param resourceLoader 클래스 패스상의 자원을 검색하기 위한 {@link ClassLoader}
	 * @throws Exception {@link Document} 생성 Exception
	 */
	public static Document loading(
			String location, ResourceLoader resourceLoader) throws Exception {
		File xmlFile = new File(location);
		Document document = null;

		if (xmlFile.exists()) {
			document = loading(xmlFile);
		} else if (resourceLoader.getResource(location) != null) {
			document = loading(resourceLoader
					.getResourceAsStream(location));
		}
		return document;
	}


	/**
	 * 파일 정보를 이용하여 {@link Document}를 생성한다.
	 * XML에 대한 유효성 검증을 수행하지 않는다.
	 *
	 * @param stream		 XML 정보를 담고 있는 {@link InputStream}
	 * @param namespaceAware namespace 정보 활성화 상태
	 * @throws Exception {@link Document} 생성 Exception
	 */
	public static Document loading(InputStream stream, boolean namespaceAware) throws Exception {
		DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
		setAllowJavaEncoding(factory);
		factory.setNamespaceAware(namespaceAware);
		DocumentBuilder builder = factory.newDocumentBuilder();
		Document document = builder.parse(stream);

		cachedXPathAPIs.put(document, new CachedXPathAPI());

		return document;
	}

	/**
	 * {@link InputStream}을 이용하여 {@link Document}를 생성한다.
	 * XML Schema 기반의 유효성 검증을 수행한다.
	 *
	 * @param stream   XML 정보를 담고 있는 {@link InputStream}
	 * @param validate XML의 유효성 검증
	 * @param resolver 스키마를 검증하고 얻어오는 {@link EntityResolver}
	 * @throws Exception {@link Document} 생성 Exception
	 */
	public static Document loading(InputStream stream, boolean validate, EntityResolver resolver) throws Exception {
		// stream 형태의 contents 를 읽어서, DOM 정보를 생성함
		DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
		setAllowJavaEncoding(factory);
		if (validate) {
			factory.setValidating(validate);
			factory.setNamespaceAware(validate);
			factory.setAttribute(SCHEMA_LANGUAGE_ATTRIBUTE, XSD_SCHEMA_LANGUAGE);
		}

		DocumentBuilder builder = factory.newDocumentBuilder();
		if (resolver != null) {
			builder.setEntityResolver(resolver);
		}
		builder.setErrorHandler(new DefaultErrorHandler());

		Document document = builder.parse(stream);

		cachedXPathAPIs.put(document, new CachedXPathAPI());

		return document;
	}

	/**
	 * {@link Document}의 XML을 파일로 출력한다.
	 * 저장되는 XML의 인코딩은 기본 값인 utf-8로 설정된다.
	 *
	 * @param document {@link Document} 객체
	 * @param filename {@link Document}를 저장할 파일명
	 */
	public static void printXML(Document document, String filename) {
		try {
			OutputStream output = new FileOutputStream(filename);
			XMLSerializer.create(output).serialize(document);
		} catch (Exception except) {
			log.debug("file not found " + filename);
		}
	}

	/**
	 * {@link Document}를 {@link StringBuffer}로 출력한다.
	 * 저장되는 XML의 인코딩은 기본 값인 utf-8로 설정된다.
	 *
	 * @param document {@link Document} 객체
	 */
	public static StringBuffer printXML(Document document) {
		StringWriter writer = new StringWriter();
		XMLSerializer.create(writer).serialize(document);
		return writer.getBuffer();
	}

	/**
	 * apache 의 xmlParser 에서 java-encodings 형식을 지원하도록 설정
	 */
	private static void setAllowJavaEncoding(DocumentBuilderFactory docBuilderFactory) {
		String docBuilderFactoryClass = docBuilderFactory.getClass().getName();
		if (docBuilderFactoryClass.startsWith("org.apache.xerces")) {
			try {
				docBuilderFactory.setFeature(
						"http://apache.org/xml/features/allow-java-encodings", true);
			} catch (ParserConfigurationException except) {
				log.info("feature(allow-java-encodings) is not supported, caused by " + except, except);
			} catch (AbstractMethodError error) {
				log.info("feature(allow-java-encodings) is not supported. Use xerces 2.8 or above");
			}
		} else {
			log.debug("XML Parser is not xerces(" + docBuilderFactoryClass + ")");
		}
	}

	// -- deprecated ----------------------------------------------------------

	/**
	 * {@link XPathAPI}를 활용한 {@link Node}를 검색한다.
	 *
	 * @param element 노드 검색을 시작할 {@link Element}
	 * @param key	 검색할 노드에 대한 XPath
	 */
	@Deprecated
	public static Node getNode(Node element, String key) {
		if (element == null) {
			return null;
		}
		Node selected = null;
		try {
			CachedXPathAPI xpathAPI = getXPathApi(element);
			synchronized (xpathAPI) {
				selected = xpathAPI.selectSingleNode(element, key);
			}
		} catch (TransformerException e) {
			log.error(key + " node is not searched!!");
		}
		return selected;
	}

	/**
	 * {@link XPathAPI}를 활용한 {@link NodeList}를 검색한다.
	 *
	 * @param element 노드 검색을 시작할 {@link Element}
	 * @param key	 검색할 노드에 대한 XPath
	 */
	@Deprecated
	public static NodeList getNodes(Node element, String key) {
		if (element == null) {
			return null;
		}
		try {
			CachedXPathAPI xpathAPI = getXPathApi(element);
			synchronized (xpathAPI) {
				return xpathAPI.selectNodeList(element, key);
			}
		} catch (TransformerException e) {
			log.error(key + " nodes is not searched!!", e);
		}
		return null;
	}

	public static Iterator<Node> getNodeIterator(Node element, String key) {
		return nodeIterable(getNodes(element, key)).iterator();
	}

	public static Iterator<Node> getChildIterator(Node element) {
		return nodeIterable(element.getChildNodes()).iterator();
	}

	/**
	 * {@link XPathAPI}의 검색 결과인 {@link Node} 리스트에 대한 {@link Iterable}을 반환한다.
	 *
	 * @param element 노드 검색을 시작할 {@link Element}
	 * @param key	 검색할 노드에 대한 XPath
	 */
	public static Iterable<Node> getNodeIterable(Node element, String key) {
		return nodeIterable(getNodes(element, key));
	}

	/**
	 * 주어진 {@link Element} 하위의 경로에 속한 값을 반환한다.
	 */
	@Deprecated
	public static String getValue(Node element, String key) {
		return XMLDOMUtil.evaluate(XMLDOMUtil.getNode(element, key));
	}

	/**
	 * 주어진 {@link Element} 하위의 경로에 속한 값을 반환한다.
	 */
	@Deprecated
	public static String getValue(Node element, String key, String defaultValue) {
		String value = XMLDOMUtil.evaluate(XMLDOMUtil.getNode(element, key));
		if (StringUtils.isEmpty(value)) {
			value = defaultValue;
		}
		return value;
	}

	/**
	 * 주어진 {@link Object}의 값을 반환한다.<p>
	 * {@link Object}는 아래의 Type 중 하나이며, 다른 Type일 경우 null을 반환한다.
	 * <ul>
	 * <li>{@link Element}</li>
	 * <li>{@link Attr}</li>
	 * <li>{@link Comment}</li>
	 * <li>{@link ProcessingInstruction}</li>
	 * </ul>
	 */
	@Deprecated
	public static String evaluate(Object selected) {
		if (selected == null) {
			return null;
		}

		String result = null;
		if (selected instanceof Element) {
			result = getElementText((Element) selected);
		} else if (selected instanceof Attr) {
			result = ((Attr) selected).getValue();
		} else if (selected instanceof Comment) {
			result = ((Comment) selected).getNodeValue();
		} else if (selected instanceof ProcessingInstruction) {
			result = ((ProcessingInstruction) selected).getNodeValue();
		}

		return result;
	}

	/**
	 * {@link Element}에 대한 값을 추출한다.
	 */
	@Deprecated
	public static String getElementText(Node element) {
		if (element.getFirstChild() == null) {
			return "";
		}

		// 하나의 element( Text or CDATA)만 존재할경우 해당 값을 추출후 반환
		if (element.getChildNodes().getLength() == 1) {
			Object obj = element.getChildNodes().item(0);
			if (obj instanceof org.w3c.dom.Text) {
				return ((Text) obj).getNodeValue();
			} else {
				return "";
			}
		}

		// 하나 이상의 element가 존재할경우 하위노드 순환하면서 값을 생성함
		StringBuffer textContent = new StringBuffer();
		boolean hasText = false;
		for (int i = 0; i < element.getChildNodes().getLength(); i++) {
			Object obj = element.getChildNodes().item(i);
			if (obj instanceof Text) {
				textContent.append(((Text) obj).getNodeValue());
				hasText = true;
			}
		}

		if (!hasText) {
			return "";
		}
		return textContent.toString();
	}

	/**
	 * {@link Node} 하위에 존재하는 param 요소 정보를 추출한다.
	 *
	 * @param parentNode params를 하위로 가지는 상위 노드
	 * @param namespace  노드 Namespace
	 */
	@Deprecated
	public static Map<String, String> getParams(Node parentNode, String namespace) {
		Map<String, String> params = new HashMap<String, String>();

		String xpath = StringUtils.isNotEmpty(namespace) ?
				namespace + ":params/" + namespace + ":param" : "params/param";
		NodeList paramNodes = XMLDOMUtil.getNodes(parentNode, xpath);
		if (paramNodes.getLength() == 0) {
			xpath = StringUtils.isNotEmpty(namespace) ? namespace + ":param" : "param";
			paramNodes = XMLDOMUtil.getNodes(parentNode, xpath);
		}
		for (int subIdx = 0; subIdx < paramNodes.getLength(); subIdx++) {
			Node paramNode = paramNodes.item(subIdx);
			String propName = XMLDOMUtil.getValue(paramNode, "@name");
			String propValue = XMLDOMUtil.getValue(paramNode, "@value");
			if (propValue == null) {
				propValue = XMLDOMUtil.evaluate(paramNode);
			}

			params.put(propName, propValue);
		}

		return params;
	}

	/**
	 * {@link NodeList}에 속하는 {@link Node}들의 snapshot을 List 형태로 반환한다.
	 */
	public static List<Node> nodeList(NodeList list) {
		List<Node> nodeList = new ArrayList<Node>(list.getLength());
		for (Node node : nodeIterable(list)) {
			nodeList.add(node);
		}
		return nodeList;
	}

	/**
	 * {@link NodeList}에 대한 {@link Iterable}를 반환한다.
	 */
	public static Iterable<Node> nodeIterable(final NodeList list) {
		return new Iterable<Node>() {
			public Iterator<Node> iterator() {
				return list == null ? Collections.<Node>emptyList().iterator() : nodeIterator(list);
			}
		};
	}

	/**
	 * {@link NodeList}에 대한 {@link Iterator}를 반환한다.
	 */
	public static Iterator<Node> nodeIterator(final NodeList list) {
		return list == null ? Collections.<Node>emptyList().iterator() : new Iterator<Node>() {
			private int counter;

			public boolean hasNext() {
				return counter < list.getLength();
			}

			public Node next() {
				return list.item(counter++);
			}

			public void remove() {
				throw new UnsupportedOperationException("remove");
			}
		};
	}

	/**
	 * {@link Document} 에 대한 {@link CachedXPathAPI} 반환.
	 */
	private static CachedXPathAPI getXPathApi(Node element) {
		Document document = element.getOwnerDocument() != null ?
				element.getOwnerDocument() : (Document) element;

		CachedXPathAPI xpathApi = cachedXPathAPIs.get(document);
		if (xpathApi == null) {
			log.debug("create new XPath context for " + document);
			xpathApi = new CachedXPathAPI();
			cachedXPathAPIs.put(document, xpathApi);
		}

		return xpathApi;
	}

	/**
	 * {@link Document}에 대한 {@link CachedXPathAPI}를 제거한다.
	 */
	public static boolean destroyDocument(Document document) {
		return cachedXPathAPIs.remove(document) != null;
	}
}
